package edu.unice.simul.modele;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;

import javax.swing.event.EventListenerList;

import edu.unice.simul.conf.GeneralSettings;
import edu.unice.simul.controleur.SimulationControleur;
import edu.unice.simul.events.MobilesMovedEvent;
import edu.unice.simul.listeners.FinalStatsListener;
import edu.unice.simul.listeners.MobilesChangedListener;
import edu.unice.simul.listeners.TimeListener;
import edu.unice.simul.modele.contenus.immobiles.Sortie;
import edu.unice.simul.modele.contenus.mobiles.Humain;
import edu.unice.simul.modele.moteur.Moteur;
import edu.unice.simul.util.Dim;
import edu.unice.simul.util.stats.StatsContainer;

/**
 * Modèle principal de l'application. Il correspond à un thread indépendant de
 * la vue qui calcule les nouvelles positions des humains.
 * 
 * @author Romain Ciaccafava
 * 
 */
public class Simulation implements Runnable {
	private EventListenerList listeners = new EventListenerList();

	protected Thread threadSimulation = null;

	private Piece piece;
	/**
	 * Vitesse actuelle de la simulation
	 */
	private long vitesseSimulation;
	private Moteur moteur = new Moteur(false);
	/**
	 * Utilisés pour les statistiques
	 */
	private Hashtable<Integer, Double> evolutionVivants = new Hashtable<Integer, Double>(),
			prcVieCumules = new Hashtable<Integer, Double>();
	/**
	 * Étape en cours
	 */
	private int etape = 0;

	/**
	 * Créé le modèle principal de la simulation
	 * 
	 * @param piece
	 *          pièce déjà initialisée
	 * @param delaiSimulation
	 *          vitesse de base de la simulation
	 * @param controleur
	 *          contrôleur de la simulation
	 */
	public Simulation(Piece piece, long delaiSimulation,
			SimulationControleur controleur) {
		this.piece = piece;
		this.vitesseSimulation = delaiSimulation;
		moteur.setControleur(controleur);
	}

	/**
	 * Démare la simulation si elle a été mise en pause
	 */
	public void demarrer() {
		threadSimulation = new Thread(this);
		threadSimulation.start();
	}

	/**
	 * Met en pause la simulation si elle est en marche
	 */
	public void arreter() {
		threadSimulation = null;
	}

	/**
	 * Lance le thread
	 */
	@Override
	public void run() {
		int nbHumains = piece.getNumberOfMobiles();
		fireNumberOfMobilesChanged(nbHumains);

		Thread currentThread = Thread.currentThread();
		while (threadSimulation == currentThread) {
			synchronized (piece) {
				etape++;
				long t1 = System.currentTimeMillis();
				moteur.pasSuivant(piece);

				if (GeneralSettings.DEBUG)
					System.err.println("Temps de calcul d'une itération : "
							+ (double) (System.currentTimeMillis() - t1) / 1000.
							+ " seconde(s)");

				fireMobilesChanged();
				if (piece.getNumberOfMobiles() != nbHumains) {
					fireNumberOfMobilesChanged(piece.getNumberOfMobiles());
					nbHumains = piece.getNumberOfMobiles();
				}

				fireTimeChanged();

				int enVie = 0;
				double prcTotalVie = 0;

				List<Humain> humains = piece.getHumains();

				for (Humain h : humains) {
					if (h.enVie()) {
						enVie++;
						prcTotalVie += h.getPrcVie();
					} else if (h.getTempsMort() == 0.)
						h.setTempsMort(moteur.getTime() / 1000);
				}

				prcTotalVie /= humains.size();

				evolutionVivants.put(etape, (double) enVie);
				prcVieCumules.put(etape, (double) prcTotalVie);

				if (moteur.estFini()) {
					fireFinalStats();
					arreter();
				}
			}
			try {
				Thread.sleep(vitesseSimulation);
			} catch (InterruptedException e) {
			}
		}
	}

	public void addMobilesChangedListener(MobilesChangedListener listener) {
		listeners.add(MobilesChangedListener.class, listener);
	}

	public void removeMobilesChangedListener(MobilesChangedListener listener) {
		listeners.remove(MobilesChangedListener.class, listener);
	}

	public void addTimeListener(TimeListener listener) {
		listeners.add(TimeListener.class, listener);
	}

	public void removeTimeListener(TimeListener listener) {
		listeners.remove(TimeListener.class, listener);
	}

	public void addFinalStatsListener(FinalStatsListener listener) {
		listeners.add(FinalStatsListener.class, listener);
	}

	public void removeFinalStatsListener(FinalStatsListener listener) {
		listeners.remove(FinalStatsListener.class, listener);
	}

	public void fireMobilesChanged() {
		MobilesChangedListener[] listenerList = (MobilesChangedListener[]) listeners
				.getListeners(MobilesChangedListener.class);

		for (MobilesChangedListener listener : listenerList) {
			listener.mobilesChanged(new MobilesMovedEvent(this, piece.getHumains()));
		}
	}

	public void fireNumberOfMobilesChanged(int newNumber) {
		MobilesChangedListener[] listenerList = (MobilesChangedListener[]) listeners
				.getListeners(MobilesChangedListener.class);

		for (MobilesChangedListener listener : listenerList) {
			listener.numberOfMobilesChanged(newNumber);
		}
	}

	public void fireTimeChanged() {
		TimeListener[] listenerList = (TimeListener[]) listeners
				.getListeners(TimeListener.class);

		for (TimeListener listener : listenerList) {
			listener.updateTime(moteur.getTime());
		}
	}

	public void fireFinalStats() {
		FinalStatsListener[] listenerList = (FinalStatsListener[]) listeners
				.getListeners(FinalStatsListener.class);

		for (FinalStatsListener listener : listenerList) {
			listener.sendFinalStats(buildStats());
		}
	}

	/**
	 * Construit les statistiques à destination de la vue.
	 */
	private StatsContainer buildStats() {
		StatsContainer container = new StatsContainer();

		container.setPersonnesMortes(piece.getNombreDeMorts());
		container.setPersonnesEnVie(piece.getNombreDeVivants());

		container.setEvolutionVivants(evolutionVivants);

		Hashtable<Integer, Double> tempsDeSorties = new Hashtable<Integer, Double>();
		double tempsDeSortieCumules = 0., tempsDeSortieCumulesCarre = 0.;
		double premierMort = 0., totalMorts = 0.;
		double meilleurTempsDeSortie = 0.;
		double pireTempsDeSortie = 0.;
		int n = 1;
		for (Humain h : piece.getHumains()) {
			if (h.estSorti()) {
				tempsDeSorties.put(n++, h.getTempsDeSortie());
				if (h.getTempsDeSortie() > pireTempsDeSortie)
					pireTempsDeSortie = h.getTempsDeSortie();
				if (meilleurTempsDeSortie == 0
						|| h.getTempsDeSortie() < meilleurTempsDeSortie)
					meilleurTempsDeSortie = h.getTempsDeSortie();
			} else if (!h.enVie()) {
				if (premierMort == 0. || premierMort > h.getTempsMort())
					premierMort = h.getTempsMort();
				totalMorts += h.getTempsMort();
			}
		}
		container.setTempsDeSortie(tempsDeSorties);

		HashMap<Dim, Integer> sorties = new HashMap<Dim, Integer>();
		for (Sortie s : piece.getSorties())
			sorties.put(s.getPosition(), 0);
		int sizeSortis = 0;
		for (Humain h : piece.getHumains()) {
			if (h.estSorti()) {
				sorties
						.put(h.getPorteDeSortie(), sorties.get(h.getPorteDeSortie()) + 1);
				tempsDeSortieCumules += h.getTempsDeSortie();
				tempsDeSortieCumulesCarre += Math.pow(h.getTempsDeSortie(), 2);
				sizeSortis++;
			}
		}
		Hashtable<String, Double> portesDeSortie = new Hashtable<String, Double>();
		for (Dim d : sorties.keySet())
			if (sorties.get(d) > 0)
				portesDeSortie.put(d.toString(), (double) sorties.get(d));

		container.setRepartitionPortes(portesDeSortie);

		container.setPrcVieCumules(prcVieCumules);

		container.setTempsMoyenDeSortie(tempsDeSortieCumules / sizeSortis);

		container.setPireTemps(pireTempsDeSortie);

		container.setMeilleurTemps(meilleurTempsDeSortie);

		double ecartType = Math.sqrt(tempsDeSortieCumulesCarre / sizeSortis
				- Math.pow(tempsDeSortieCumules / sizeSortis, 2));
		container.setEcartType(ecartType);

		container.setTempsPremiereMort(premierMort);

		container.setTempsMoyenMort(totalMorts / piece.getNombreDeMorts());
		return container;
	}

	public void setSpeed(int speed) {
		switch (speed) {
		case 1:
			this.vitesseSimulation = GeneralSettings.VITESSE_1;
			break;
		case 2:
			this.vitesseSimulation = GeneralSettings.VITESSE_2;
			break;
		case 3:
			this.vitesseSimulation = GeneralSettings.VITESSE_3;
			break;
		case 4:
			this.vitesseSimulation = GeneralSettings.VITESSE_4;
			break;
		case 5:
		default:
			this.vitesseSimulation = GeneralSettings.VITESSE_5;
		}
	}

	/**
	 * Lance l'évacuation de la pièce
	 */
	public void lancerEvacuation() {
		moteur.lancerEvacuation();
	}
}
